package mirrormonkey.core.server;

import java.util.Arrays;

import mirrormonkey.core.ConstructorNotFoundException;
import mirrormonkey.core.member.ConstructorData;
import mirrormonkey.core.member.SearchKey;
import mirrormonkey.core.messages.EntityChangeMessage;
import mirrormonkey.core.messages.EntityEndMessage;
import mirrormonkey.core.messages.EntityInitMessage;
import mirrormonkey.framework.entity.StaticEntityData;
import mirrormonkey.framework.entity.SyncEntity;

public class ExistingMappingStackElement extends MappingStackElement {

	private final StaticEntityData staticData;

	private ConstructorData constr;

	private Object[] constrParams;

	private int refcount;

	public ExistingMappingStackElement(ExistingMappingStack stack,
			MappingStackElement prev, MappingStackElement next,
			StaticEntityData staticData, SearchKey search) {
		super(stack);
		this.staticData = staticData;
		constr = staticData.getData(search, ConstructorData.class);
		if (constr == null) {
			constrNotFoundError(search);
		}
		refcount = 1;
		insertBetween(prev, next);
		this.constrParams = search.getParameters();
	}

	protected void constrNotFoundError(SearchKey search) {
		throw new ConstructorNotFoundException(
				"Could not find constructor for:\nlocal (server) class: "
						+ staticData.localClass
						+ "\nconnected (client) class: "
						+ staticData.connectedClass + "\nkey used: " + search);
	}

	@Override
	public MappingStackElement find(Class<? extends SyncEntity> connectedClass,
			FindMode find, FailMode fail) {
		System.out.println("ExistingMappingStackElement.find(): "
				+ connectedClass + "find mode: " + find);
		if (find.equals(FindMode.EXACT) && matchesExactly(connectedClass)) {
			System.out.println("exact");
			return this;
		} else if (find.equals(FindMode.FIRST_COMPATIBLE)
				&& matchesSubclass(connectedClass)) {
			System.out.println("subclass");
			return this;
		}
		if (fail.equals(FailMode.INCOMPATIBLE)
				&& !matchesSuperclass(connectedClass)) {
			throw new ClassCastException(this + " will not accept "
					+ connectedClass);
		}
		System.out.println("prev");
		return prev.find(connectedClass, find, fail);
	}

	@Override
	public boolean exists() {
		return true;
	}

	protected void insertBetween(MappingStackElement prev,
			MappingStackElement next) {
		this.prev = prev;
		this.next = next;
		prev.next = this;
		next.prev = this;
	}

	@Override
	public MappingStackElement addRef(boolean override, SearchKey search) {
		// if this implementation is called, then this element has been found by
		// an exact search! therefore, no insert is necessary.
		System.out.println("existing element: addRef()");
		if (override) {
			System.out.println("override");
			ConstructorData c = staticData.getData(search,
					ConstructorData.class);
			if (constr == null) {
				constrNotFoundError(search);
			}
			// TODO Arrays.deepEquals() does not account for identityAware
			// parameters
			if (c.getId() != constr.getId()
					|| !Arrays.deepEquals(constrParams, this.constrParams)) {
				System.out.println("override foh realz");
				this.constr = c;
				this.constrParams = search.getParameters();
				if (!next.exists()) {
					System.out.println("notify change");
					notifyChange(false, this);
				}
			}
			System.out.println("/override");
		}
		refcount++;
		return this;
	}

	@Override
	public MappingStackElement removeRef() {
		if (refcount == 0) {
			return stack.dummyElement;
		} else if (--refcount == 0) {
			return destroy();
		}
		return this;
	}

	@Override
	public MappingStackElement cutTop() {
		while (next.exists()) {
			next.destroy();
		}
		return this;
	}

	@Override
	public MappingStackElement cutBottom() {
		while (prev.exists()) {
			prev.destroy();
		}
		return this;
	}

	@Override
	public MappingStackElement destroy() {
		System.out.println("Destroying existing element");
		refcount = 0;
		prev.next = next;
		next.prev = prev;
		if (stack.isEmpty()) {
			System.out.println("stack empty");
			notifyEnd();
			((ExistingMappingStack) stack).destroy(this);
		} else if (!next.exists()) {
			System.out.println("notify change");
			((ExistingMappingStackElement) prev).notifyChange(this);
		}
		next = null;
		prev = null;
		return stack.dummyElement;
	}

	@Override
	public boolean matchesExactly(Class<? extends SyncEntity> connectedClass) {
		return staticData.connectedClass.equals(connectedClass);
	}

	@Override
	public boolean matchesSubclass(Class<? extends SyncEntity> connectedClass) {
		return staticData.connectedClass.isAssignableFrom(connectedClass);
	}

	@Override
	public boolean matchesSuperclass(Class<? extends SyncEntity> connectedClass) {
		return connectedClass.isAssignableFrom(staticData.connectedClass);
	}

	@Override
	public StaticEntityData getStaticData() {
		return staticData;
	}

	// TODO the whole call hierarchy for the next three is extremely ugly and
	// must be cleaned up
	protected void notifyInit() {
		stack.connectionData.send(new EntityInitMessage(stack.entityData,
				constr, constrParams));
		stack.module.notifyInitListeners(stack.entityData, getStaticData(),
				stack.connectionData);
	}

	protected void notifyChange(ExistingMappingStackElement lastElement) {
		notifyChange(true, lastElement);
	}

	protected void notifyChange(boolean connectedClassChanged,
			ExistingMappingStackElement lastElement) {
		System.out.println("NOTIFY CHANGE: " + lastElement);
		// no one notifies the serverEntityData that the mapping has changed
		// (the stack will notify it on init and end), so we do it here
		stack.connectionData.send(new EntityChangeMessage(stack.entityData,
				constr, constrParams));
		stack.module.notifyChangeListeners(stack.entityData,
				lastElement.staticData, getStaticData(),
				stack.entityData.getEntity(), stack.connectionData);
		if (connectedClassChanged) {
			stack.entityData.changeMapping(stack.connectionData, lastElement,
					this);
		}
	}

	protected void notifyEnd() {
		stack.module.notifyEndListeners(stack.entityData, getStaticData(),
				stack.connectionData);
		stack.connectionData.send(new EntityEndMessage(stack.entityData));
	}

	@Override
	public String toString() {
		return "[" + getClass().getName() + ": staticData=" + staticData
				+ ", constr=" + constr + ", refcount=" + refcount + "]";
	}

}
